Flutter 使用 freezed 管理数据类
在 App 开发过程中,通常需要声明大量数据实体类(Model/Entity),最典型场景是网络请求数据数据。在 Flutter/Dart 下,因为无法使用反射,封装数据实体类不是一件容易事。社区的最佳实践是使用代码生成器,用户声明实体的主要内容,由生成器补全周边实现。freezed 是这一领域比较知名的框架,在本文中,记录了我对该框架的了解上手。
从官方文档可知,freezed 的特色体现在以下几个方面:
- 减少代 boilerplate code
- Equality 判等
- 深拷贝
- 序列化
- immutability、mutability 管理
别小看实体类,不仅仅是“JSON 转 Model”那么简单。考虑好上面几方面因素,将让软件项目的代码质量、架构质量上升一个台阶。
安装
这里以 Flutter 项目为例,需要安装以下依赖:
flutter pub add freezed_annotation
flutter pub add dev:build_runner
flutter pub add dev:freezed
如果需要 JSON 序列化转换,还需要安装 json_annotation
:
# if using freezed to generate fromJson/toJson, also add:
flutter pub add json_annotation
flutter pub add dev:json_serializable
其中:
仅在开发是使用的依赖:
- build_runner:代码生成器的执行器
- freezed:代码生成器
会编译到产物中的代码:
- freezed_annotation:用于声明 freezed 注解
这里以官方 Example 为例。
声明实体类
生成实体类
创建名为 Person 的实体类,创建 person.dart
:
之所以使用截图来展示代码,是因为在写 Person 类的代码时,会有许多标红找不到。这是正常情况,因为这些标红的,正是需要生成的。
这里有些固定写法需要注意:
'package:flutter/foundation.dart'
必须要引入part
依赖必须要声明,如果不用 json 序列化,可不用'person.g.dart'
Person
要按照这种固定写法来写
这种固定写法,是需要开发者记忆或者通过工具来生成的。并且在书写时,没有代码提示,全靠记忆。这些是无法避免的 boilerplate code。
执行代码生成器
首先,需要通过 build_runner 执行代码生成器:
dart run build_runner build
执行后,将看到两个 part 依赖文件都被生成出来,报错也都自动消除了。
编辑器插件
在上一节中说道,尽管 freezed 降低了 boilerplate code,但是仍然存在一些固定写法,在记忆上造成一些负担。为此,freezed 还提供了编辑器插件(VSCode、IntelliJ、Android Studio)。
由于我使用 VSCode,这里以 VS Code 为例。
在 VS Code 下,需要安装两个插件:
前者负责生成 freezed 类,能够自动生成 boilerplate code,将我们彻底解放。
后者负责触发 build runner,省去了自己在命令行里输入的繁琐。
在以上两个插件的加持上,开发体验非常爽了。
Immutability
freezed 提供了两个注解,分别用于声明不可变和可变对象:
@freezed
:不可变@unfreezed
:可变
本节介绍 @freezed
不可变实体类。
所谓不可变实体类,创建实例后,属性不可修改:
不仅如此,如果 @freezed
对象的属性为集合类型(List、Map、Set),它们也是不可修改的。
总之,实例创建出来之后,不可修改,修改就抛异常。
判等
看下面代码:
Person p1 = Person(firstName: "Maxiee", lastName: "Maeiee", age: 18);
// 可以复制并修改新对象的值
Person p2 = p1.copyWith();
print("p1 == p2: ${p1 == p2}"); // true
p1 == p2
返回 true。首先,他俩不是同一个实例。之所以相等,是因为代码生成器重写了判等逻辑:
// PersonImpl in person.freezed.dart
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PersonImpl &&
(identical(other.firstName, firstName) ||
other.firstName == firstName) &&
(identical(other.lastName, lastName) ||
other.lastName == lastName) &&
(identical(other.age, age) || other.age == age));
}
从中可见,不仅实例相同会认为相等,不同实例的值相等也会认为相等。
这意味着,p2 是从 p1 深拷贝出来的,但两者仍然能够比较。
深拷贝
说到深拷贝,通过 copyWith
方法实现,该方法允许接受参数,允许对成员进行修改。
上一节代码中,给出了 copyWith
的基本使用方法。
在官方文档中,给出了一个高级用例,即当有层层实体类嵌套时:
@freezed
class Company with _$Company {
factory Company({
String? name,
required Director
director}) = _Company;
}
@freezed
class Director with _$Director {
factory Director({
String? name,
Assistant? assistant}) = _Director;
}
@freezed
class Assistant with _$Assistant {
factory Assistant({
String? name,
int? age}) = _Assistant;
}
我们想更换 Company 的 Director 的 Assistant,该怎么办?伪代码如下:
Company company;
Company newCompany = company.copyWith.director.assistant(name: 'John Smith');
如果只想更换 Director 的 name?
Company company;
Company newCompany = company.copyWith.director(name: 'John Doe');
其他用法
为实体类添加方法
需要手动添加一个私有构造方法:
@freezed
class Person with _$Person {
// Added constructor. Must not have any parameter
const Person._();
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
构造断言
class Person with _$Person {
@Assert('name.isNotEmpty', 'name cannot be empty')
@Assert('age >= 0')
factory Person({
String? name,
int? age,
}) = _Person;
}
默认值
class Example with _$Example {
const factory Example([
@Default(42) int value]) = _Example;
}
跟 json_serializable
是打通的。
更复杂用法
更复杂用法还包括 Union 联合类型、泛型。我还没有研究到这么深入,请参见官方文档。
总结
freezed 还支持 @unfreezed
Mutable 类型,本文中不再赘述,感兴趣的小伙伴可参见官方文档。
网络资源
- freezed | Dart Package
- rrousselGit/freezed: Code generation for immutable classes that has a simple syntax/API without compromising on the features.
本文作者:Maeiee
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!